// ==UserScript==
// @name       		Music Unlimited Scrobbler
// @namespace  		https://music.sonyentertainmentnetwork.com/
// @version    		1.0
// @description 	Scrobbles music from Sony's Music Unlimited service.
// @copyright  		Copyright (c) 2014, Steven Derks. Protected by GNU General Public License, version 3 (GPL-3.0).
// @include      	https://music.sonyentertainmentnetwork.com/*
// @exclude			https://music.sonyentertainmentnetwork.com/sony-web/client/*.cache.html
// @require     	http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js
// @require			http://www.myersdaily.org/joseph/javascript/md5.js
// ==/UserScript==

// Authentication
var apiKey = "c9bea91fe958dadbac0689aec340eabc";
var secret = "f64c5d4238de57a61fbeabc49c4bdcd6";
var token = "";
var signature = "";
var sessionKey = "";
var loginSucceeded = false;

// Misc
var apiUrl = "https://ws.audioscrobbler.com/2.0/";
var lastFmScrobblerUrl = "http://www.last.fm/api/auth?api_key=" + apiKey + "&cb=https://music.sonyentertainmentnetwork.com";
var playerMonitor = null;
var messageDispenser = null;

// User information
var userName = "";

function init()
{
	console.log("--- Music Unlimited Scrobbler enabled ---");

	// Fetch the token from the GET data
	token = getToken();

	// If the token is set, create a new session
	if(token)
	{
		call("auth.getSession", {}, "GET", processLogin);
	}
	else
	{
		// No token was found. Check the localStorage for a session key and a username.
		sessionKey = localStorage.sessionKey;
		userName = localStorage.userName;

		if(!sessionKey)
		{
			// No session key found, a new session has to be made. Redirect
			// the user to the Last.fm page so we can create a new session.
			console.log("No session found. Redirect.")
			window.location.replace(lastFmScrobblerUrl);
		}
		else
		{
			// Session key found. We can start the PlayerMonitor straight away.
			loginSucceeded = true;
			console.log("Logged in to Last.fm as " + userName + " via preexisting session.");
			start();
		}
	}
}

function processLogin(response)
{
	var status = $(response).find("lfm").attr("status");

	if(status == "ok")
	{
		// Fetch the Last.fm username and the session key
		userName = $(response).find("name").text();
		sessionKey = $(response).find("key").text();
		loginSucceeded = true;
		console.log("Logged in to Last.fm as " + userName + ".");

		// Store the username and session key in the localStorage
		localStorage.sessionKey = sessionKey;
		localStorage.userName = userName;

		// Start the PlayerMonitor
		start();
	}
	else
	{
		console.log("Last.fm login failed! Response status: " + status + ".");
	}
}

function start()
{
	messageDispenser = new MessageDispenser();
	messageDispenser.showMessage("Logged into Last.fm as " + userName + ".");

	playerMonitor = new PlayerMonitor();
	playerMonitor.start();
}

function call(methodName, params, protocol, callback)
{
	// Add the method parameter
	params["method"] = methodName;

	// Add authentication parameters
	// If we've not yet logged in, provide the token. Otherwise provide the active session key.
	if(!loginSucceeded)
		params["token"] = token;
	else
		params["sk"] = sessionKey;
	params["api_key"] = apiKey;
	params["api_sig"] = createSignature(params);

	$.ajax({
		type: protocol,
		url: apiUrl,
		data: params
	}).done(function(response) {
		if(callback != null)
			callback(response);
	});
}

function getUnixTimestamp()
{
	return Math.round(new Date().getTime() / 1000);
}

function createSignature(params)
{
	// Fetch the parameter keys, and sort them
	var keys = Object.keys(params);
	keys.sort();

	// Construct the raw signature
	var rawSignature = "";
	for(keyIndex in keys)
	{
		var key = keys[keyIndex];
		rawSignature += key + params[key];
	}

	// Append the secret
	rawSignature += secret;

	//console.log("Signature: " + rawSignature);
	return md5(rawSignature);
}

function getToken()
{
	var urlVars = getUrlVars();
	return urlVars["token"];
}

function getUrlVars()
{
    var vars = {};
    var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
        vars[key] = value;
    });
    return vars;
}

/*
 *	Track
 */
function Track()
{
	// Fetch misc track information
	this.name = $("div.GEKKVSQBP- a").attr("title");
	this.artist = $("div.GEKKVSQBCY a").attr("title");
	this.album = $("div.GEKKVSQBH- a").attr("title");

	// Set the duration
	this.updateDuration();
}

Track.prototype.updateDuration = function()
{
	// Fetch the duration of the track
	var duration = $("span#PlayerDuration").text(); // minutes:seconds

	// Split the duration into minutes & seconds
	var durationSplit = duration.split(":");
	var minutes = durationSplit[0];
	var seconds = durationSplit[1];

	// Set the duration
	this.duration = Number(minutes * 60) + Number(seconds);
};

Track.prototype.updateNowPlaying = function()
{
	var params = {
		track: this.name,
		artist: this.artist,
		album: this.album,
		duration: this.duration
	};

	call("track.updateNowPlaying", params, "POST", null);
	console.log("Update now playing: " + this.toString());
};

Track.prototype.scrobble = function()
{
	var params = {
		track: this.name,
		artist: this.artist,
		album: this.album,
		timestamp: getUnixTimestamp()
	};

	call("track.scrobble", params, "POST", null);
	console.log("Scrobbling: " + this.toString());
};

Track.prototype.equals = function(other)
{
	return this.name == other.name &&
		this.artist == other.artist &&
		this.album == other.album;
};

Track.prototype.isEmpty = function()
{
	return this.name == null &&
		this.artist == null &&
		this.album == null;
}

Track.prototype.toString = function()
{
	return "Name: " + this.name + ", artist: " + this.artist + ", album: " + this.album
		+ ", duration: " + this.duration + " sec.";
}

/*
 *	PlayerMonitor
 */ 
function PlayerMonitor()
{
	// Track monitoring & management
	this.track = null;
	this.currentTrackRunningTime = 0;
	this.minimumScrobbleRunningTime = 30000; // 30 sec

	// Updating
	this.threadId = -1;
	this.interval = 3000; //ms

	// State management
	this.state = PlayerState.Paused;
	this.stateTable = {
		"-479px -25px": PlayerState.Playing, 	// Pause button
		"-433px -62px": PlayerState.Playing,	// Pause button highlighted
    	"-433px -25px": PlayerState.Paused,		// Play button
    	"-387px -62px": PlayerState.Paused		// Play button highlighted
	};

	// Misc
	this.enabled = false;
}

PlayerMonitor.prototype.start = function()
{
	if(this.enabled)
		return; // Return if we're already running

	this.enabled = true;

	// Set the current track as the "last track" for monitoring
	this.track = new Track();
	if(!this.track.isEmpty())
		this.track.updateNowPlaying();

	// Save a reference to this PlayerMonitor for use in the setInterval
	// function below. In said function, "this" will refer to the setInteral
	// thread rather than the PlayerMonitor. Because we still need access
	// to this PlayerMonitor, we'll save its reference as "self" for later
	// use.
	self = this

	this.threadId = setInterval(function()
	{
		if(!self.enabled)
			clearInterval(self.threadId);

		self.update();
	}, this.interval);
};

PlayerMonitor.prototype.stop = function()
{
	this.enabled = false;
};

PlayerMonitor.prototype.update = function()
{
	// Fetch the current state
	var currentState = this.getState();

	// Add running time for the current track if we're playing
	if(currentState == PlayerState.Playing)
	{
		this.currentTrackRunningTime += this.interval;
	}

	// Fire any events if required
	if(this.state == PlayerState.Playing && currentState == PlayerState.Paused)
		this.onPaused();
	else if(this.state == PlayerState.Paused && currentState == PlayerState.Playing)
		this.onResumed();

	// Replace the PlayerMonitor state with the current state
	this.state = currentState;

	// Check for track switching. We will only consider a track as being switched
	// when the full duration has also been loaded. Music Unlimited won't load the
	// duration of a song, unless you click the play button first. We will assume
	// the same behaviour, and only register a track change when the full duration
	// has been loaded.
	var currentTrack = new Track();
	if(!this.track.equals(currentTrack) && currentTrack.duration != 0)
	{
		this.onTrackChanged(this.track, currentTrack);
		this.track = currentTrack;
		this.currentTrackRunningTime = 0;
	}
};

PlayerMonitor.prototype.onTrackChanged = function(oldTrack, newTrack)
{
	console.log("Track changed to: " + newTrack.toString());
	if(this.currentTrackRunningTime >= this.minimumScrobbleRunningTime)
	{
		oldTrack.scrobble();
	}

	newTrack.updateNowPlaying();
	messageDispenser.showScrobbling(newTrack);
};

PlayerMonitor.prototype.onPaused = function()
{
	//console.log("Paused.");
};

PlayerMonitor.prototype.onResumed = function()
{
	//console.log("Resumed.");
};

PlayerMonitor.prototype.getState = function()
{
	var backgroundPos = $("img#PlayerPlayPause").css("background-position");
	return this.stateTable[backgroundPos];
};

/*
 * PlayerState
 */
var PlayerState =
{
    Playing: 0,
    Paused: 1

};
Object.freeze(PlayerState);

/*
 *	MessageDispenser
 */
function MessageDispenser()
{
	this.messageDuration = 5000; //ms
	this.fadeDuration = 400; //ms

	// Create the message box
	this.createMessageBox();
}

MessageDispenser.prototype.createMessageBox = function()
{
	var body = $("body");

	// Create the popup box
	this.msgBox = $('<div id="msgBox"></div>');
	$(this.msgBox).css("position", "fixed");
	$(this.msgBox).css("background-color", "rgb(255, 255, 255)");
	$(this.msgBox).css("bottom", "0px");
	$(this.msgBox).css("right", "0px");
	$(this.msgBox).css("border", "1px solid #CCCCCC");
	$(this.msgBox).css("z-index", "999");
	$(this.msgBox).css("text-align", "center");
	$(this.msgBox).css("display", "none"); // Disable display by default
	$(this.msgBox).appendTo(body);

	// Create the content table within the msgBox
	this.msgBoxContent = $('<table id="msgBoxContent"><tbody><tr></tr></tbody></table>');
	$(this.msgBoxContent).css("border-collapse", "collapse");
	$(this.msgBoxContent).css("border-spacing", "0px");
	$(this.msgBoxContent).css("width", "100%");
	$(this.msgBoxContent).css("height", "100%");
	$(this.msgBoxContent).appendTo(this.msgBox);

	// Create the cell that will display the actual message text
	this.msgBoxText = $('<td id="msgBoxText"></td>');
	$(this.msgBoxText).css("vertical-align", "middle");
	$(this.msgBoxText).css("padding", "5px");
	$(this.msgBoxText).css("font-size", "10pt");
	$(this.msgBoxText).css("min-width", "150px");
	$(this.msgBoxText).appendTo($(this.msgBoxContent).find("tr"));

	// Create the Last.fm logo cell
	this.msgBoxLogoContainer = $('<td id="msgBoxLogoContainer"></td>');
	$(this.msgBoxLogoContainer).css("border-left", "1px solid #CCCCCC");
	$(this.msgBoxLogoContainer).css("background-color", "#DE1C23");
	$(this.msgBoxLogoContainer).css("min-width", "100px");
	$(this.msgBoxLogoContainer).insertAfter(this.msgBoxText);

	// Create the Last.fm logo div
	this.msgBoxLogo = $('<div id="msgBoxLogo"><div>');
	$(this.msgBoxLogo).css("width", "90px");
	$(this.msgBoxLogo).css("height", "30px");
	$(this.msgBoxLogo).css("background-image", "url(http://cdn.lst.fm/flatness/header/lastfm_logo_red.png)");
	$(this.msgBoxLogo).css("background-position", "0px 30px");
	$(this.msgBoxLogo).css("clip", "rect(0px, 90px, 30px, 0px)");
	$(this.msgBoxLogo).css("margin", "auto");
	$(this.msgBoxLogo).appendTo(this.msgBoxLogoContainer);

	// Create the Last.fm username box
	this.msgBoxUserName = $('<div id="msgBoxUserName"><div>');
	$(this.msgBoxUserName).css("color", "white");
	$(this.msgBoxUserName).css("margin", "3px");
	$(this.msgBoxUserName).text(userName);
	$(this.msgBoxUserName).appendTo(this.msgBoxLogoContainer);
}

MessageDispenser.prototype.showScrobbling = function(track)
{
	var message = $("<div></div>");

	// Create the scrobbling header
	var msgBoxScrobblingHeader = $('<span id="msgBoxScrobblingHeader">Scrobbling:</span><br/>');
	$(msgBoxScrobblingHeader).css("font-weight", "bold");
	$(msgBoxScrobblingHeader).css("margin-bottom", "bold");
	$(msgBoxScrobblingHeader).appendTo(message);

	var msgBoxScrobblingTrack = $('<span id="msgBoxScrobblingTrack">' + track.name + ' - ' + track.artist + '</span>');
	$(msgBoxScrobblingTrack).appendTo(message);

	this.showMessage(message);
}

MessageDispenser.prototype.showMessage = function(message)
{
	this.clear();
	$(this.msgBoxText).html(message); // If the message is a DOM-object, it will be automatically converted to html

	var self = this;
	$(this.msgBox).fadeIn(this.fadeDuration, function()
	{
		setTimeout(function()
		{
			$(self.msgBox).fadeOut(self.fadeDuration); // Use "self", because "this" will refer to the setTimeout thread
		}, self.messageDuration);
	});
}

MessageDispenser.prototype.clear = function()
{
	$(this.msgBoxText).empty();
}

init(); // Initialize